﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace Effects.Wave
{
    /// <summary>
    ///  Exception thrown while reading when input file doesn't have supported format
    /// </summary>
    /// <remarks>
    /// <para>
    ///  At the moment project support only PCM WAVE files - no compression. Support for other formats might be implemnted via writing 
    ///  your own WaveReaders and WaveWriters that will understand other formats and creates common instance of Wave class.
    ///  </para>
    /// </remarks>
    public class InvalidFileFormatException : Exception
    {
        /// <summary>
        ///  Creates new instance with specified string message.
        /// </summary>
        /// <param name="s"> Message </param>
        public InvalidFileFormatException(string s)
            : base(s)
        { }

        /// <summary>
        ///  Creates new instance with implicit message string: "Invalid format of the input file"
        /// </summary>
        public InvalidFileFormatException()
            : base("Invalid format of the input file")
        { }
    }

    /// <summary>
    ///  Class representing wave file
    /// </summary>
    /// <remarks> 
    /// <para>
    ///  This class wraps the wave file and provides access to it's data. It implements iterators that runs throught the samples or indexers that returns samples
    ///  </para>
    /// </remarks>
    public class Wave
    {
        #region data_chunk
        // data chunk of the wrapped file

        /// <summary>
        ///  Data Chunk
        /// </summary>
        protected DataChunk dataChunk;

        /// <summary>
        ///  Gets Data chunk of the Wave file
        /// </summary>
        public DataChunk DataChunk
        {
            get { return dataChunk; }
        }

        #endregion

        #region format_chunk
        // format chunk of the wrapped file

        /// <summary>
        ///  Format Chunk
        /// </summary>
        protected FmtChunk fmtChunk;
        /// <summary>
        ///  Gets Format Chunk of the Wave file
        /// </summary>
        public FmtChunk FmtChunk
        {
            get { return fmtChunk; }
        }

        /// <summary>
        ///  Maximal value of the sample.
        /// </summary>
        private int maxValue;

        #endregion

        #region length 
        // length of the wrapped file

        /// <summary>
        ///  length of a file with offset 8 - not including "RIFF" identificator and length itself
        /// </summary>
        protected uint length;
        /// <summary>
        ///  Gets the length of a file with offset 8 - not including "RIFF" identificator and length itself
        /// </summary>
        public uint Lenght
        {
            get { return length; }
        }

        /// <summary>
        ///  Length of the file in samples.
        /// </summary>
        private int sampleLength;

        /// <summary>
        ///  Gets the length of the file in samples.
        /// </summary>
        public long SampleLength
        {
            get
            {
               return sampleLength;
            }
        }

        #endregion

        #region constructor
        // construcotr

        /// <summary>
        ///  Creates new Wave with given DataChunk, FmtChunk and length
        /// </summary>
        /// <param name="dChunk"> data chunk of the wave </param>
        /// <param name="fChunk">  format chunk of the wave </param>
        /// <param name="length"> length of the wave file in bytes  </param>
        /// <remarks>
        ///  Length of the wave is always with offset 8, it ignores first 8 bytes (RIFF id and length itself)
        /// </remarks>
        public Wave(DataChunk dChunk, FmtChunk fChunk, uint length)
        {
            this.dataChunk = dChunk;
            this.fmtChunk = fChunk;
            this.length = length;
            this.sampleLength = Convert.ToInt32(dataChunk.Data.Length / fmtChunk.Channels ); 

            this.appendBuffer = new List<double>[fmtChunk.Channels];
            for(int i=0;i<fmtChunk.Channels;i++)
                this.appendBuffer[i] = new List<double>();

            this.maxValue = Convert.ToInt32(Math.Pow(2,fmtChunk.BitsPerSample - 1));
        }

        #endregion

        #region enumerators

        /// <summary>
        ///  Iterator returns samples as doubles, 1 - max value that can be represented 
        /// </summary>
        /// <param name="channel"> Channel from which samples are iterated </param>
        /// <returns> Enumerator that runs throught the samples of chosen channel.</returns>
        /// <remarks>
        ///  This method provides very useful feature. Enumerators return smaples as double in range [-1,1] and so returned values are independetn of bit depth of the file's bit depth.
        ///  That allows to process different wave formats with same routines. 
        /// </remarks>
        public System.Collections.IEnumerable GetSampleDoubleEnumerator(int channel)
        {
            if (channel < 0 || channel >= fmtChunk.Channels)
                throw new ArgumentException("Channel out of range");
            
            int skip = 0;

            foreach (double sample in dataChunk.Data)
            {
                if (skip == channel)
                    yield return (sample);

                skip++;
                if (skip >= fmtChunk.Channels)
                    skip = 0;
            }
            yield break;
        }

        #endregion

        #region creating_new_data_chunk
        // stuff for creating new data

        /// <summary>
        /// Buffer used when constructing new wave file.
        /// </summary>
        private List<double>[] appendBuffer;

        /// <summary>
        ///  Adds data to the buffer. To use current state of the buffer and create new <see cref="DataChunk"/> use <see cref="Wave.Append"/> method.
        /// </summary>
        /// <param name="data"> Data to add </param>
        /// <param name="channel"> Channel to which are data added. </param>
        public void ToAppend(double[] data,int channel)
        {
            foreach(double d in data)
                appendBuffer[channel].Add(d);
        }

        /// <summary>
        ///  Creates new <see cref="DataChunk"/> from buffer. Buffer can be filled by <see cref="Wave.ToAppend"/> method.
        /// </summary>
        public void Append()
        {
            double[] samples = new double[appendBuffer.Length * appendBuffer[0].Count];
            for (int i = 0; i < appendBuffer[0].Count; i++)
            {
                for (int channel = 0; channel < fmtChunk.Channels; channel++)
                {
                    samples[FmtChunk.Channels * i + channel] = appendBuffer[channel][i];
                }
            }

            this.dataChunk = new DataChunk(samples);
            this.length = fmtChunk.Lenght + 16 + dataChunk.Lenght * fmtChunk.BitsPerSample / 8;
            this.sampleLength = (int) dataChunk.Lenght /  fmtChunk.Channels;
        }

        #endregion

        #region get_sample
        // accessing samples 

        /// <summary>
        ///  Gets sample at specified index from given channel.
        /// </summary>
        /// <param name="channel"> Channel </param>
        /// <param name="index"> Index of the sample  </param>
        /// <returns> Samples at position index from given channel </returns>
        public double getSample(int channel, long index)
        {
            if (channel < 0 || channel >= fmtChunk.Channels)
                return 0;
            //throw new ArgumentException("Illegal channel index. Channel " + channel + " doesn't exists.");

            if (index < 0 || index >= this.dataChunk.Data.Length/FmtChunk.Channels)
                return 0;
            //throw new ArgumentException("Illegal sample index. Sample number " + index + " doesn't exists.");

            int realIndex = Convert.ToInt32(fmtChunk.Channels * index + channel);
            return this.dataChunk.Data[realIndex];
        }

        #endregion

    }
}
